package com.hucsmn.chatroom;

import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.AccessDeniedException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.*;

/**
 * 聊天室服务器。
 */
public class ChatServer implements Runnable {
    /**
     * 默认绑定地址 0.0.0.0。
     */
    public static final InetAddress ADDRESS;

    /**
     * 默认绑定端口 8888。
     */
    public static final int PORT = 8888;

    /**
     * 获取请求头信息时，套接字的超时时间。
     */
    private static final int TIMEOUT = 5000;

    static {
        InetAddress address = null;
        try {
            address = Inet4Address.getByAddress(new byte[]{0, 0, 0, 0});
        } catch (Exception e) {
            e.printStackTrace();
        }
        ADDRESS = address;
    }

    // 监听地址。
    private final InetAddress address;
    private final int port;

    // 聊天室状态。
    private final File directory;
    private final ExecutorService pool = Executors.newCachedThreadPool();
    private final ConcurrentMap<String, Peer> peers = new ConcurrentHashMap<>();
    private final ConcurrentMap<String, File> files = new ConcurrentHashMap<>();

    /**
     * 聊天室在线成员记录。
     */
    private static class Peer {
        private final Socket socket;
        private final BufferedReader in;
        private final Writer out;

        /**
         * 创建聊天室在线成员记录。
         *
         * @param socket 客户端套接字。
         * @throws IOException 获取套接字读写数据流异常。
         */
        public Peer(Socket socket) throws IOException {
            if (socket == null) {
                throw new NullPointerException();
            }

            this.socket = socket;
            this.in = new BufferedReader(
                    new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
            this.out = new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8);
        }

        /**
         * 监听来自客户端的下一条消息。
         *
         * @return 读取到的下一条消息，客户端关闭时返回 null。
         * @throws IOException 发生网络异常。
         */
        public String listen() throws IOException {
            return in.readLine();
        }

        /**
         * 向客户端通知消息。
         *
         * @param who 消息来源。
         * @param message 消息内容。
         * @throws IOException 发生网络异常。
         */
        public void tell(String who, String message) throws IOException {
            if (socket.isClosed()) {
                return;
            }
            message = message.replace('\n', ' ');
            out.write(who + ": " + message + "\n");
            out.flush();
        }
    }

    /**
     * 创建聊天室服务器，使用默认地址和端口号。
     *
     * @throws IOException 无法创建临时文件夹。
     */
    ChatServer() throws IOException {
        this(ADDRESS, PORT);
    }

    /**
     * 创建聊天室服务器，使用指定端口号和默认地址。
     *
     * @param port 监听端口号。
     * @throws IOException 无法创建临时文件夹。
     */
    ChatServer(int port) throws IOException {
        this(ADDRESS, port);
    }

    /**
     * 创建聊天室服务器，使用指定地址和端口号。
     *
     * @param address 监听地址。
     * @param port 监听端口号。
     * @throws IOException 无法创建临时文件夹。
     */
    ChatServer(InetAddress address, int port) throws IOException {
        this(address, port, Files.createTempDirectory("chat-server").toFile());
    }

    /**
     * 创建聊天室服务器，使用指定地址、端口号、文件储存位置。
     *
     * @param address 监听地址。
     * @param port 监听端口号。
     * @param directory 文件储存位置。
     * @throws IOException 文件储存位置非法。
     */
    ChatServer(InetAddress address, int port, File directory) throws IOException {
        if (address == null || directory == null) {
            throw new NullPointerException();
        }
        if (port <= 0) {
            throw new IllegalArgumentException();
        }
        if (directory.isFile()) {
            throw new FileAlreadyExistsException(directory.getPath());
        }
        if (!directory.exists()) {
            if (!directory.mkdirs()) {
                throw new AccessDeniedException(directory.getPath());
            }
        }

        this.directory = directory;
        this.address = address;
        this.port = port;
    }

    /**
     * 启动聊天室服务器。
     */
    @Override
    public void run() {
        try (ServerSocket sock = new ServerSocket()) {
            sock.bind(new InetSocketAddress(address, port));
            while (true) {
                Socket request = sock.accept();
                request.setSoTimeout(TIMEOUT);
                pool.execute(() -> handle(request));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 处理客户端的各类请求。
     *
     * @param request 请求套接字。
     */
    private void handle(Socket request) {
        try (request) {
            InputStream in = request.getInputStream();
            String[] header = ChatHeaders.readHeader(in);
            if (header.length == 0) {
                throw new MalformedChatHeaderException("miss connection type");
            }

            request.setSoTimeout(0);
            switch (header[0].toUpperCase()) {
                case "CHAT":
                    // 聊天登录。
                    chat(request, header);
                    break;
                case "SEND":
                    // 发送文件，等待接收。
                    send(request, header);
                    break;
                case "RECV":
                    // 接收文件。
                    receive(request, header);
                    break;
                default:
                    System.err.println(request.getRemoteSocketAddress() +
                            " 发送了非法请求：" + Arrays.toString(header));
                    throw new MalformedChatHeaderException("bad connection type");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 处理登陆聊天请求。
     *
     * @param request 请求套接字。
     * @param header 请求头信息。
     * @throws IOException 网络异常。
     */
    private void chat(Socket request, String[] header) throws IOException {
        System.err.println(request.getRemoteSocketAddress() +
                " 请求加入聊天室，头消息为：" + Arrays.toString(header));

        // 取一个昵称。
        String nick = fallbackNick(request.getRemoteSocketAddress());
        boolean fallback = true;
        if (header.length >= 2) {
            for (int i = 1; i < header.length; i++) {
                String alternative = header[i];
                if (peers.get(alternative) == null) {
                    nick = alternative;
                    fallback = false;
                    break;
                }
            }
        }

        Peer self = new Peer(request);
        peers.put(nick, self);

        try {
            // 返回登陆提示，并广播登录消息。

            if (!fallback) {
                self.tell("系统提示", "欢迎你加入群聊 " + nick);
            } else {
                self.tell("系统提示", "由于昵称重复，系统已自动为您命名为 " + nick);
            }
            broadcast("系统提示", nick + " 加入了群聊", nick);

            // 接收并向其他客户端广播消息。
            String message;
            while ((message = self.listen()) != null) {
                broadcast(nick, message, nick);
            }
        } finally {
            // 退出群聊，并向其他成员广播下线通知。
            peers.remove(nick);
            broadcast("系统提示", nick + " 退出了群聊");
        }
    }

    /**
     * 处理文件发送请求。
     *
     * @param request 请求套接字。
     * @param header 请求头信息。
     * @throws IOException 广播消息时发生网络异常。
     */
    private void send(Socket request, String[] header) throws IOException {
        System.err.println(request.getRemoteSocketAddress() +
                " 请求发送文件，头消息为：" + Arrays.toString(header));

        // 取临时文件名和默认文件名。
        Instant now = Instant.now();
        String tmpName = now.getEpochSecond() + "_" + now.getNano() + ".tmp";
        String name = now.getEpochSecond() + "_" + now.getNano() + ".dat";

        try {
            // 接收文件。
            File tmpFile = new File(directory, tmpName);
            try (OutputStream out = new FileOutputStream(tmpFile)) {
                InputStream in = request.getInputStream();
                StreamCopy.copy(in, out);
            }

            // 根据备选文件名列表改名，并放进待接收表中。
            if (header.length >= 2) {
                for (int i = 1; i < header.length; i++) {
                    String alternative = header[i];
                    if (files.get(alternative) == null) {
                        name = alternative;
                        break;
                    }
                }
            }
            File file = new File(directory, name);
            if (!tmpFile.renameTo(file)) {
                throw new IOException("cannot rename '" + tmpName + "' to '" + file + "'");
            }
            files.put(name, file);
        } catch (IOException e) {
            // 返回失败消息。
            try {
                System.err.println("文件接收失败：" + name);
                ChatHeaders.writeHeader(request.getOutputStream(), "FAIL", e.toString());
            } catch (IOException e2) {
                e2.printStackTrace();
            }
            throw e;
        }

        // 返回成功，并广播文件等待接收的消息。
        System.err.println("文件接收成功：" + name);
        ChatHeaders.writeHeader(request.getOutputStream(), "SUCCESS", name);
        broadcast("系统提示", "待接收文件 " + name);
    }

    /**
     * 处理文件接收请求。
     *
     * @param request 请求套接字。
     * @param header 请求头信息。
     * @throws IOException 头字段不正确、文件不存在或已经被接收，以及网络异常。
     */
    private void receive(Socket request, String[] header) throws IOException {
        System.err.println(request.getRemoteSocketAddress() +
                " 请求接收文件，头消息为：" + Arrays.toString(header));

        if (header.length != 2) {
            throw new MalformedChatHeaderException("bad receive header argument");
        }

        // 查找等待接收表。
        String name = header[1];
        File file = files.get(name);
        if (file == null) {
            // 返回文件不存在。
            try {
                System.err.println("文件 '" + name + "' 不存在");
                ChatHeaders.writeHeader(request.getOutputStream(), "NOTFOUND", name);
            } catch (IOException e2) {
                e2.printStackTrace();
            }
            throw new FileNotFoundException(name);
        }

        try (InputStream in = new FileInputStream(file)) {
            // 写入表示文件存在的消息头。
            System.err.println("发送文件 '" + name + "'...");
            OutputStream out = request.getOutputStream();
            ChatHeaders.writeHeader(out, "FOUND", name);

            // 转发数据，并广播文件被成功接收的消息。
            StreamCopy.copy(in, out);
            System.err.println("文件 '" + name + "' 发送成功！");
            broadcast("系统提示", "文件已被接收 " + name);
        }
    }

    /**
     * 向所有聊天室成员广播消息。
     *
     * @param who 消息来源。
     * @param message 消息内容。
     * @throws IOException 发生网络异常。
     */
    private void broadcast(String who, String message) throws IOException {
        for (Map.Entry<String, Peer> entry : peers.entrySet()) {
            Peer peer = entry.getValue();
            peer.tell(who, message);
        }
    }

    /**
     * 向除了某个成员外，所有的聊天室成员广播消息。
     *
     * @param who 消息来源。
     * @param message 消息内容。
     * @param exclude 排除成员。
     * @throws IOException 发生网络异常。
     */
    private void broadcast(String who, String message, String exclude) throws IOException {
        for (Map.Entry<String, Peer> entry : peers.entrySet()) {
            if (Objects.equals(entry.getKey(), exclude)) {
                continue;
            }
            Peer peer = entry.getValue();
            peer.tell(who, message);
        }
    }

    /**
     * 获取默认昵称。
     *
     * @param address 客户端地址。
     * @return 默认昵称。
     */
    private String fallbackNick(SocketAddress address) {
        String nick = Integer.toString(Math.abs(address.toString().hashCode()), 36);
        if (peers.containsKey(nick)) {
            Random rng = new Random();
            do {
                nick = Integer.toString(Math.abs(rng.nextInt()), 36);
            } while (peers.containsKey(nick));
        }
        return nick;
    }

    /**
     * 获取监听地址。
     *
     * @return 监听地址。
     */
    public InetAddress getAddress() {
        return address;
    }

    /**
     * 获取监听端口号。
     *
     * @return 监听端口号。
     */
    public int getPort() {
        return port;
    }

    /**
     * 获取缓存文件夹位置。
     *
     * @return 缓存文件夹位置。
     */
    public File getDirectory() {
        return directory;
    }

    /**
     * 删除整个缓存文件夹。
     *
     * @return 缓存文件夹是否已彻底删除。
     */
    public boolean deleteDirectory() {
        File[] files = directory.listFiles();
        if (files != null) {
            boolean purge = true;
            for (File file : files) {
                purge &= file.delete();
            }
            if (purge) {
                return directory.delete();
            }
        }
        return false;
    }
}
